跳到主要内容

SpringCloud 搜索服务

数据的导入

Spring Data 介绍

Spring Data 是一个用于简化数据库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持 map-reduce 框架和云计算数据服务。Spring Data 可以极大的简化 JPA 的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了 CRUD 外,还包括如分页、排序等一些常用的功能。

Spring Data的官网:http://projects.spring.io/spring-data/

SpringData ES 介绍

Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch 操作,将原始操作 elasticSearch 的客户端API 进行封装。

Spring Data 为 Elasticsearch 项目提供集成搜索引擎。Spring Data Elasticsearch POJO 的关键功能区域为中心的模型与 ElasticSearch 交互文档和轻松地编写一个存储库数据访问层。

官方网站:http://projects.spring.io/spring-data-elasticsearch/

选择对应的版本:

例如这里使用的 ElasticSearch 版本是 5.6.8

对应的 spring-data-elasticsearch 版本是 3.0.6

数据导入流程

  1. 请求 Search 服务,调用数据导入地址
  2. 根据注册中心中的注册的 Goods 服务的地址,使用 Feign 方式查询所有已经审核的 sku
  3. 使用 SpringData ES 将查询到的 Sku 集合导入到 ES 中

数据从MySQL导入到ES中大概分为以下几个步骤:

搜索工程搭建

创建搜索微服务工程,该工程主要提供搜索服务以及索引数据的更新操作。

添加依赖

<!--SpringDataES依赖-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>3.0.6.RELEASE</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>jcl-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-core</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
</exclusions>
</dependency>

编写配置文件

server:
port: 18086
spring:
application:
name: search
data:
elasticsearch:
cluster-name: my-application # 集群节点的名称,就是在es的配置文件中配置的
cluster-nodes: 192.168.211.132:9300 # 这里用的是TCP端口所以是9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#超时配置
ribbon:
ReadTimeout: 300000 # Feign请求读取数据超时时间

hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000 # feign连接超时时间

编写启动类

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.robod.goods.feign")
@EnableElasticsearchRepositories(basePackages = "com.robod.mapper")
public class SearchApplication {

public static void main(String[] args) {
//解决SpringBoot的netty和elasticsearch的netty相关jar冲突
System.setProperty("es.set.netty.runtime.available.processors", "false");
SpringApplication.run(SearchApplication.class,args);
}
}

创建 ES 的 JavaBean

首先我们需要去创建一个 JavaBean 来定义相关的映射配置,Index,Type,Field。

changgou-service-search-apicom.robod.entity 包下创建一个 JavaBean 叫 SkuInfo:

@Data
@Document(indexName = "sku_info", type = "docs")
public class SkuInfo implements Serializable {

@Id
private Long id;//商品id,同时也是商品编号

/**
* SKU名称
* FieldType.Text支持分词
* analyzer 创建索引的分词器
* searchAnalyzer 搜索时使用的分词器
*/
@Field(type = FieldType.Text, analyzer = "ik_smart",searchAnalyzer = "ik_smart")
private String name;

@Field(type = FieldType.Double)
private Long price;//商品价格,单位为:元

private Integer num;//库存数量

private String image;//商品图片

private String status;//商品状态,1-正常,2-下架,3-删除

@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createTime;//创建时间

@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime updateTime;//更新时间

private String isDefault; //是否默认

private Long spuId;//SPU_ID

private Long categoryId;//类目ID

@Field(type = FieldType.Keyword)
private String categoryName;//类目名称,不分词

@Field(type = FieldType.Keyword)
private String brandName;//品牌名称,不分词

private String spec;//规格

private Map<String, Object> specMap;//规格参数

}

在 SkuInfo 中,设置了 Index 是 "sku_info",Type 为 "docs",并为几个字段设置了分词。然后在 changgou-service-goods-apicom.robod.goods.feign 包下创建一个 Feign 的接口 SkuFeign

@FeignClient(name = "goods")
@RequestMapping("/sku")
public interface SkuFeign {

/**
* 查询所有的sku数据
* @return
*/
@GetMapping
Result<List<Sku>> findAll();
}

我们将使用这个 Feign 去调用 Goods 微服务中的 findAll 方法去数据库中获取所有的 Sku 数据。

编写导入功能实现代码

最后,在 changgou-service-search 微服务中写出 Controller,Service,Dao 层的相关代码,实现数据导入的功能。

//SkuEsController
@GetMapping("/import")
public Result importData(){
skuEsService.importData();
return new Result(true, StatusCode.OK,"数据导入成功");
}
-----------------------------------------------------------
//SkuEsServiceImpl
@Override
public void importData() {
List<Sku> skuList = skuFeign.findAll().getData();
List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(skuList), SkuInfo.class);
//将spec字符串转化成map,map的key会自动生成Field
for (SkuInfo skuInfo : skuInfos) {
Map<String,Object> map = JSON.parseObject(skuInfo.getSpec(),Map.class);
skuInfo.setSpecMap(map);
}
skuEsMapper.saveAll(skuInfos);
}
-------------------------------------------------------------
//继承自 ElasticsearchRepository,泛型为SkuInfo,主键类型为Long
// 注意:这里是接口
public interface SkuEsMapper extends ElasticsearchRepository<SkuInfo,Long> {
}

功能实现

根据关键词搜索

先封装一个 Entity 来作为前后端传参的格式:

@Data
public class SearchEntity {

private long total; //搜索结果的总记录数

private int totalPages; //查询结果的总页数

private List<SkuInfo> rows; //搜索结果的集合

public SearchEntity() {
}

public SearchEntity(List<SkuInfo> rows, long total, int totalPages) {
this.rows = rows;
this.total = total;
this.totalPages = totalPages;
}
}

然后就是在搜索微服务中写出相应的代码了

@GetMapping
public Result<SearchEntity> searchByKeywords(@RequestParam(required = false)String keywords) {
SearchEntity searchEntity = skuEsService.searchByKeywords(keywords);
return new Result<>(true,StatusCode.OK,"根据关键词搜索成功",searchEntity);
}

---------------------------------------------------------------------------------------------------
@Override
public SearchEntity searchByKeywords(String keywords) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();

if (!StringUtils.isEmpty(keywords)) {
nativeSearchQueryBuilder.withQuery(QueryBuilders.queryStringQuery(keywords).field("name"));
}

AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate
.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class);
List<SkuInfo> content = skuInfos.getContent();
return new SearchEntity(content,skuInfos.getTotalElements(),skuInfos.getTotalPages());
}

分类统计

当我们在小米商城上面搜索一件商品的时候,下面会将分类展示出来帮助用户进一步地筛选产品。在畅购商城的表设计中,也有一个叫 categoryName 的字段。接下来就是要实现把我们搜索出来的数据进行分类统计。

我们要实现的就是图中的效果,只不过是在 Elasticsearch 中而不是 MySQL。

修改 SearchEntity,添加一个 categoryList 字段:

private List<String> categoryList;  //分类集合

修改 SkuEsServiceImpl 中的 searchByKeywords 方法,添加分组统计的的代码:

public SearchEntity searchByKeywords(String keywords) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
if (!StringUtils.isEmpty(keywords)) {
nativeSearchQueryBuilder.withQuery(QueryBuilders.queryStringQuery(keywords).field("name"));
//terms: Create a new aggregation with the given name.
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("categories_grouping")
.field("categoryName"));
}
NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate.queryForPage(nativeSearchQuery, SkuInfo.class);
StringTerms stringTerms = skuInfos.getAggregations().get("categories_grouping");
List<String> categoryList = new ArrayList<>();
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
categoryList.add(bucket.getKeyAsString());
}
return new SearchEntity(skuInfos.getTotalElements(),skuInfos.getTotalPages(),
categoryList,skuInfos.getContent());
}

Reference

畅购商城(五):Elasticsearch实现商品搜索